const std = @import("std");
const c = @cImport({
    @cInclude("uv.h");
});

const assert = std.debug.assert;
const zeroes = std.mem.zeroes;

pub const Error = error{
    TooBig,
    PermissionDenied,
    AddressNotAvailable,
    AddressFamilyNotSupported,
    TemporaryFailure,
    BadFlags,
    BadHints,
    RequestCanceled,
    Failure,
    AiFamilyNotSupported,
    OutOfMemory,
    NoAddress,
    UnknownService,
    Overflow,
    UnknownProtocol,
    ServiceNotAvailable,
    SocketTypeNotSupported,
    AlreadyInProgress,
    BadFileDescriptor,
    ResourceBusy,
    OperationCanceled,
    InvalidUnicodeCharacter,
    ConnectionAborted,
    ConnectionRefused,
    ConnectionReset,
    DestinationAddressRequired,
    AlreadyExists,
    BadAdressInSyscall,
    FileTooLarge,
    HostUnreachable,
    InterruptedSyscall,
    InvalidArgument,
    IOError,
    AlreadyConnected,
    IsDirectory,
    TooManySymlinks,
    TooManyOpenFiles,
    MessageTooLong,
    NameTooLong,
    NetworkDown,
    NetworkUnreachable,
    FileTableOverflow,
    NoBufferSpace,
    DeviceNotFound,
    FileNotFound,
    MachineUnreachable,
    ProtocolNotAvailable,
    NoSpaceLeft,
    FunctionNotImplemented,
    NotConnected,
    NotADirectory,
    DirectoryNotEmpty,
    NotSocket,
    NotSupported,
    ValueTooLarge,
    BrokenPipe,
    ProtocolError,
    ProtocolNotSupported,
    ProtocolWrongType,
    ResultTooLarge,
    ReadOnlyFileSystem,
    NotRunning,
    InvalidSeek,
    NoSuchProcess,
    Timeout,
    TextFileBusy,
    CrossDeviceLinkNotPermitted,
    UnknownError,
    EndOfFile,
    NoSuchDeviceOrAddress,
    TooManyLinks,
    NotTTY,
    WrongFileType,
    IllegalByteSequence,
    NotAttached,
};

pub fn check(res: isize) Error!void {
    if (res >= 0) {
        return;
    }

    return switch (res) {
        c.UV_E2BIG => Error.TooBig,
        c.UV_EACCES, c.UV_EPERM => Error.PermissionDenied,
        c.UV_EADDRNOTAVAIL => Error.AddressNotAvailable,
        c.UV_EAFNOSUPPORT,
        c.UV_EAI_ADDRFAMILY,
        => Error.AddressFamilyNotSupported,
        c.UV_EAGAIN, c.UV_EAI_AGAIN => Error.TemporaryFailure,
        c.UV_EAI_BADFLAGS => Error.BadFlags,
        c.UV_EAI_BADHINTS => Error.BadHints,
        c.UV_EAI_CANCELED => Error.RequestCanceled,
        c.UV_EAI_FAIL => Error.Failure,
        c.UV_EAI_FAMILY => Error.AiFamilyNotSupported,
        c.UV_EAI_MEMORY, c.UV_ENOMEM => Error.OutOfMemory,
        c.UV_EAI_NODATA => Error.NoAddress,
        c.UV_EAI_NONAME => Error.UnknownService,
        c.UV_EAI_OVERFLOW => Error.Overflow,
        c.UV_EAI_PROTOCOL => Error.UnknownProtocol,
        c.UV_EAI_SERVICE => Error.ServiceNotAvailable,
        c.UV_EAI_SOCKTYPE,
        c.UV_ESOCKTNOSUPPORT,
        => Error.SocketTypeNotSupported,
        c.UV_EALREADY => Error.AlreadyInProgress,
        c.UV_EBADF => Error.BadFileDescriptor,
        c.UV_EBUSY => Error.ResourceBusy,
        c.UV_ECANCELED => Error.OperationCanceled,
        c.UV_ECHARSET => Error.InvalidUnicodeCharacter,
        c.UV_ECONNABORTED => Error.ConnectionAborted,
        c.UV_ECONNREFUSED => Error.ConnectionRefused,
        c.UV_ECONNRESET => Error.ConnectionReset,
        c.UV_EDESTADDRREQ => Error.DestinationAddressRequired,
        c.UV_EEXIST => Error.AlreadyExists,
        c.UV_EFAULT => Error.BadAdressInSyscall,
        c.UV_EFBIG => Error.FileTooLarge,
        c.UV_EHOSTUNREACH => Error.HostUnreachable,
        c.UV_EINTR => Error.InterruptedSyscall,
        c.UV_EINVAL => Error.InvalidArgument,
        c.UV_EIO => Error.IOError,
        c.UV_EISCONN => Error.AlreadyConnected,
        c.UV_EISDIR => Error.IsDirectory,
        c.UV_ELOOP => Error.TooManySymlinks,
        c.UV_EMFILE => Error.TooManyOpenFiles,
        c.UV_EMSGSIZE => Error.MessageTooLong,
        c.UV_ENAMETOOLONG => Error.NameTooLong,
        c.UV_ENETDOWN => Error.NetworkDown,
        c.UV_ENETUNREACH => Error.NetworkUnreachable,
        c.UV_ENFILE => Error.FileTableOverflow,
        c.UV_ENOBUFS => Error.NoBufferSpace,
        c.UV_ENODEV => Error.DeviceNotFound,
        c.UV_ENOENT => Error.FileNotFound,
        c.UV_ENONET => Error.MachineUnreachable,
        c.UV_ENOPROTOOPT => Error.ProtocolNotAvailable,
        c.UV_ENOSPC => Error.NoSpaceLeft,
        c.UV_ENOSYS => Error.FunctionNotImplemented,
        c.UV_ENOTCONN => Error.NotConnected,
        c.UV_ENOTDIR => Error.NotADirectory,
        c.UV_ENOTEMPTY => Error.DirectoryNotEmpty,
        c.UV_ENOTSOCK => Error.NotSocket,
        c.UV_ENOTSUP => Error.NotSupported,
        c.UV_EOVERFLOW => Error.ValueTooLarge,
        c.UV_EPIPE => Error.BrokenPipe,
        c.UV_EPROTO => Error.ProtocolError,
        c.UV_EPROTONOSUPPORT => Error.ProtocolNotSupported,
        c.UV_EPROTOTYPE => Error.ProtocolWrongType,
        c.UV_ERANGE => Error.ResultTooLarge,
        c.UV_EROFS => Error.ReadOnlyFileSystem,
        c.UV_ESHUTDOWN => Error.NotRunning,
        c.UV_ESPIPE => Error.InvalidSeek,
        c.UV_ESRCH => Error.NoSuchProcess,
        c.UV_ETIMEDOUT => Error.Timeout,
        c.UV_ETXTBSY => Error.TextFileBusy,
        c.UV_EXDEV => Error.CrossDeviceLinkNotPermitted,
        c.UV_EOF => Error.EndOfFile,
        c.UV_ENXIO => Error.NoSuchDeviceOrAddress,
        c.UV_EMLINK => Error.TooManyLinks,
        c.UV_ENOTTY => Error.NotTTY,
        c.UV_EFTYPE => Error.WrongFileType,
        c.UV_EILSEQ => Error.IllegalByteSequence,
        c.UV_EUNATCH => Error.NotAttached,
        else => Error.UnknownError,
    };
}

fn Cast(Self: type, DataType: ?type) type {
    const info = @typeInfo(Self).Struct;
    const Data = if (DataType) |Data| Data else anyopaque;

    comptime {
        assert(info.layout == .@"extern");
        assert(info.fields.len == 1);
    }

    const Inner = info.fields[0].type;

    comptime {
        assert(@sizeOf(Self) == @sizeOf(Inner));
        assert(@alignOf(Self) == @alignOf(Inner));
    }

    return struct {
        pub inline fn toUv(self: *Self) *Inner {
            return @ptrCast(self);
        }

        pub inline fn fromUv(ptr: *Inner) *Self {
            return @ptrCast(ptr);
        }

        pub inline fn getLoop(self: *Self) *Loop {
            return Loop.fromUv(self.toUv().loop);
        }

        pub inline fn setData(self: *Self, data: ?*Data) void {
            self.toUv().data = data;
        }

        pub inline fn getData(self: *Self) ?*Data {
            return @ptrCast(@alignCast(self.toUv().data));
        }

        pub fn close(
            self: *Self,
            comptime callback: ?*const fn (*Self) void,
        ) void {
            c.uv_close(
                @ptrCast(self),
                if (callback) |cb|
                    struct {
                        fn func(handle: ?*c.uv_handle_t) callconv(.C) void {
                            cb(Self.fromUv(@ptrCast(handle.?)));
                        }
                    }.func
                else
                    null,
            );
        }
    };
}

const RunMode = enum(c_uint) {
    default = c.UV_RUN_DEFAULT,
    once = c.UV_RUN_ONCE,
    nowait = c.UV_RUN_NOWAIT,
};

pub const Loop = extern struct {
    const Self = @This();

    inner: c.uv_loop_t = zeroes(c.uv_loop_t),

    pub usingnamespace Cast(Self, null);

    pub fn default() *Self {
        return Self.fromUv(c.uv_default_loop());
    }

    pub fn run(self: *Self, mode: RunMode) !void {
        try check(c.uv_run(self.toUv(), @intFromEnum(mode)));
    }

    pub fn deinit(self: *Self) void {
        _ = c.uv_loop_close(self.toUv());
    }
};

pub fn TimerTyped(DataType: ?type) type {
    return extern struct {
        const Self = @This();

        inner: c.uv_timer_t = zeroes(c.uv_timer_t),

        pub usingnamespace Cast(Self, DataType);

        pub fn init(self: *Self, loop: *Loop) !void {
            try check(c.uv_timer_init(loop.toUv(), self.toUv()));
        }

        pub fn start(
            self: *Self,
            comptime callback: *const fn (*Self) void,
            timeout: u64,
            repeat: u64,
        ) !void {
            try check(c.uv_timer_start(
                self.toUv(),
                struct {
                    fn func(timer: ?*c.uv_timer_t) callconv(.C) void {
                        callback(Self.fromUv(timer.?));
                    }
                }.func,
                timeout,
                repeat,
            ));
        }

        pub fn stop(self: *Self) void {
            _ = c.uv_timer_stop(self.toUv());
        }
    };
}
